﻿using System;
using System.Reflection;
using System.Reflection.Emit;
using Property.ILGen;

namespace Property.DelegateBased
{
    /// <summary>
    /// Provides IL level implementation of new type's properties, based on TImplementation-povided delegates.
    /// </summary>
    /// <typeparam name="TBaseType"></typeparam>
    /// <typeparam name="TImplementation"></typeparam>
    public class PropertyImplementer<TBaseType,TImplementation> : IPropertyImplementer where TImplementation:class,IImplenemtationMap<TBaseType>,new()
    {
        private static readonly MethodInfo ImplementGetterMethod = typeof(IImplenemtationMap<TBaseType>)
            .GetMethod("ImplementGetter", BindingFlags.Public | BindingFlags.Instance);
        private static readonly MethodInfo ImplementSetterMethod = typeof(IImplenemtationMap<TBaseType>)
            .GetMethod("ImplementSetter", BindingFlags.Public | BindingFlags.Instance);

        private static readonly MethodInfo OverrideGetterMethod = typeof(IImplenemtationMap<TBaseType>)
            .GetMethod("OverrideGetter", BindingFlags.Public | BindingFlags.Instance);
        private static readonly MethodInfo OverrideSetterMethod = typeof(IImplenemtationMap<TBaseType>)
            .GetMethod("OverrideSetter", BindingFlags.Public | BindingFlags.Instance);



        private static readonly MethodInfo GetTypeFromHandle = typeof (Type).GetMethod("GetTypeFromHandle");
        private static readonly MethodInfo GetProperty = typeof(Type).GetMethod("GetProperty",new Type[]{typeof(String)});


        
        private ConstructorBuilder _cctor;
        private LocalBuilder _mapLocal;
        private ILGenerator _cctorIl;
        private int _counter = 0;

        protected TypeBuilder TypeBuilder { get; set; }

        #region Implementation of IPropertyGetterImplementer



        public void ImplementGetter(MethodBuilder methodBuilder, PropertyInfo property)
        {
            #region cctor emit

            var delegateType = typeof (Func<,>).MakeGenericType(typeof (TBaseType), property.PropertyType);

            var delegateField = TypeBuilder.DefineField(
                String.Format("get_{2}.{0}#{3}",property.Name,property.PropertyType.Name,property.DeclaringType.FullName,_counter++)
                ,delegateType
                ,FieldAttributes.Private | FieldAttributes.Static | FieldAttributes.InitOnly);

            //load pointer to Implementation
            _cctorIl.Emit(OpCodes.Ldloc,_mapLocal);

            //_cctorIL.Emit(OpCodes.Ldstr, property.Name);

            //emit runtime type handle
            _cctorIl.Emit(OpCodes.Ldtoken, property.DeclaringType);

            _cctorIl.Emit(OpCodes.Call, GetTypeFromHandle);

            _cctorIl.Emit(OpCodes.Ldstr, property.Name);

            _cctorIl.Emit(OpCodes.Call, GetProperty);

            var genericMethod = property.DeclaringType.IsAssignableFrom(TypeBuilder.BaseType)
                                    ? OverrideGetterMethod
                                    : ImplementGetterMethod;

            var metod = genericMethod.MakeGenericMethod(TypeBuilder.BaseType, property.DeclaringType, TypeBuilder, property.PropertyType);

            _cctorIl.Emit(OpCodes.Callvirt,metod);

            _cctorIl.Emit(OpCodes.Stsfld,delegateField);
            
            #endregion

            #region  Getter Emit

            var il = methodBuilder.GetILGenerator();
            
            il.Emit(OpCodes.Ldsfld,delegateField);

            il.Emit(OpCodes.Ldarg_0);

            if (typeof(TBaseType).IsInterface)
            {
                il.Emit(OpCodes.Castclass, typeof (TBaseType));
            }

            var invokeMethod = delegateType.GetMethod("Invoke");

            il.Emit(OpCodes.Tailcall);

            il.Emit(OpCodes.Callvirt,invokeMethod);
            
            il.Emit(OpCodes.Ret);

            #endregion

        }

        #endregion

        #region Implementation of IPropertySetterImplementer



        public void ImplementSetter(MethodBuilder methodBuilder, PropertyInfo property)
        {
            #region cctor emit

            var delegateType = typeof(Action<,>).MakeGenericType(typeof(TBaseType), property.PropertyType);

            var delegateField = TypeBuilder.DefineField(
                String.Format("set_{2}.{0}#{3}", property.Name, property.PropertyType.Name, property.DeclaringType.FullName, _counter++)
                , delegateType
                , FieldAttributes.Private | FieldAttributes.Static | FieldAttributes.InitOnly);

            //load pointer to Implementation
            _cctorIl.Emit(OpCodes.Ldloc, _mapLocal);

            //_cctorIL.Emit(OpCodes.Ldstr,property.Name);

            //emit runtime type handle
            _cctorIl.Emit(OpCodes.Ldtoken,property.DeclaringType);

            
            _cctorIl.Emit(OpCodes.Call,GetTypeFromHandle);

            _cctorIl.Emit(OpCodes.Ldstr,property.Name);

            _cctorIl.Emit(OpCodes.Call,GetProperty);

            var genericMethod = property.DeclaringType.IsAssignableFrom(TypeBuilder.BaseType)
                                    ? OverrideSetterMethod
                                    : ImplementSetterMethod;

            var metod = genericMethod.MakeGenericMethod(TypeBuilder.BaseType, property.DeclaringType, TypeBuilder, property.PropertyType);

            //getting implementation
            _cctorIl.Emit(OpCodes.Callvirt, metod);

            _cctorIl.Emit(OpCodes.Stsfld, delegateField);

            #endregion

            #region  Setter Emit

            var il = methodBuilder.GetILGenerator();
            
            il.Emit(OpCodes.Ldsfld, delegateField);

            il.Emit(OpCodes.Ldarg_0);

            if (typeof(TBaseType).IsInterface)
            {
                il.Emit(OpCodes.Castclass, typeof (TBaseType));
            }

            il.Emit(OpCodes.Ldarg_1);

            var invokeMethod = delegateType.GetMethod("Invoke");

            il.Emit(OpCodes.Tailcall);

            il.Emit(OpCodes.Callvirt, invokeMethod);

            il.Emit(OpCodes.Ret);

            #endregion
        }

        #endregion

        #region Implementation of IPropertyImplementer

        public virtual void BeginEmit(TypeBuilder builder)
        {

            if(TypeBuilder != null)
            {
                throw new InvalidOperationException("Emit is already initialized.");
            }

            TypeBuilder = builder;

            var baseType = typeof(TBaseType);
            if (!baseType.IsAssignableFrom(TypeBuilder.BaseType))
            {
                throw new ArgumentException("TBaseType is not assignable from TypeBuilder.BaseType", "_inType");
            }



            //define static constructor
            _cctor = TypeBuilder.DefineConstructor(MethodAttributes.Static,CallingConventions.Standard,new Type[0]);
            _cctorIl = _cctor.GetILGenerator();
            
            _mapLocal = _cctorIl.DeclareLocal(typeof (IImplenemtationMap<TBaseType>));

            var ctor = typeof (TImplementation).GetConstructor(new Type[0]);

            //emit construction of implementation instance
            _cctorIl.Emit(OpCodes.Newobj,ctor);

            _cctorIl.Emit(OpCodes.Castclass,typeof(IImplenemtationMap<TBaseType>));
            
            //set field
            _cctorIl.Emit(OpCodes.Stloc,_mapLocal);

        }

        virtual public void EndEmit()
        {
            if(TypeBuilder == null)
            {
                throw new InvalidOperationException();
            }
            
            _cctorIl.Emit(OpCodes.Ret);
            
            TypeBuilder = null;
        }

        #endregion
    }
}
